package com.stox.client.ui.javafx.chartingy.model.plot;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.paint.Color;

import com.stox.client.api.core.Client;
import com.stox.client.ui.javafx.chartingy.model.axis.DateAxis;
import com.stox.client.ui.javafx.chartingy.model.chart.Chart;
import com.stox.client.ui.javafx.chartingy.model.enumeration.BarType;
import com.stox.client.ui.javafx.chartingy.model.node.AreaDataNode;
import com.stox.client.ui.javafx.chartingy.model.node.Candle;
import com.stox.client.ui.javafx.chartingy.model.node.CriclePnfNode;
import com.stox.client.ui.javafx.chartingy.model.node.CrossPnfNode;
import com.stox.client.ui.javafx.chartingy.model.node.DataNode;
import com.stox.client.ui.javafx.chartingy.model.node.HLC;
import com.stox.client.ui.javafx.chartingy.model.node.KagiNode;
import com.stox.client.ui.javafx.chartingy.model.node.LineBreakNode;
import com.stox.client.ui.javafx.chartingy.model.node.LinePlotNode;
import com.stox.client.ui.javafx.chartingy.model.node.OHLC;
import com.stox.client.ui.javafx.chartingy.model.node.PriceBox;
import com.stox.client.ui.javafx.chartingy.model.node.PriceLineDataNode;
import com.stox.client.ui.javafx.chartingy.model.node.RenkoNode;
import com.stox.client.ui.javafx.chartingy.view.ChartingView;
import com.stox.client.ui.javafx.core.StoxPlatform;
import com.stox.shared.charting.model.Pnf;
import com.stox.shared.charting.util.BarTypeUtil;
import com.stox.shared.core.model.Bar;
import com.stox.shared.core.model.Scrip;
import com.stox.shared.core.model.filter.BarCriteria;
import com.stox.shared.core.util.MathUtil;
import com.stox.shared.core.util.StringUtil;

public class PricePlot extends Plot<Scrip, Bar> {

	private double volumeSum;
	private List<Bar> loadedBars;
	private List<Bar> bars; // This is translated data
	
	private Color upBarColor = Color.GREEN;
	private Color downBarColor = Color.RED;


	public PricePlot(ChartingView chartingView) {
		super(chartingView);
		extraValueProperty().addListener(new ChangeListener<Object>() {
			@Override
			public void changed(final ObservableValue<? extends Object> observable, final Object oldValue, final Object extraValue) {
				updateScrip();
			}
		});
	}

	public List<Bar> getBars() {
		return bars;
	}

	public List<Bar> getLoadedBars() {
		return loadedBars;
	}

	public void updateScrip() {
		final Scrip scrip = getExtraValue();
		if (null != scrip) {
			displayNameProperty().set(scrip.getName() + "(" + scrip.getId() + ")");
			if (isPrimary()) {
				chartingView().currentBarCriteria().setScripId(scrip.getId());
				chartingView().getTitleBar().setTitleText(getDisplayName());
			}
		}
	}


	@Override
	public void updateAxisBounds() {
		double max = Double.MIN_VALUE;
		double min = Double.MAX_VALUE;
		final DateAxis dateAxis = primaryChart().getDateAxis();
		final int lowerBoundIndex = dateAxis.getLowerBoundIndex();
		final int upperBoundIndex = dateAxis.getUpperBoundIndex();
		volumeSum = 0;
		for (int index = lowerBoundIndex; index >= upperBoundIndex; index--) {
			if (index < dataProperty().getSize()) {
				final Data<Scrip, Bar> data = dataProperty().get(index);
				volumeSum += data.getExtraValue().getVolume();
				max = Math.max(max, data.getMax());
				min = Math.min(min, data.getMin());
			}
		}
		setMin(min);
		setMax(max);
		final Chart<Scrip, Bar> chart = getChart();
		chart.setMin(Math.min(chart.getMin(), min));
		chart.setMax(Math.max(chart.getMax(), max));
	}


	private int lastLowerBoundIndex = 0;
	private int lastUpperBoundIndex = 0;
	private final Calendar calendar = Calendar.getInstance();
	private final DateFormat yearDateFormat = new SimpleDateFormat("yyyy");
	private final DateFormat monthDateFormat = new SimpleDateFormat("MMM");

	@Override
	public void layoutChartChildren() {
		final DateAxis dateAxis = primaryChart().getDateAxis();
		final int lowerBoundIndex = dateAxis.getLowerBoundIndex();
		final int upperBoundIndex = dateAxis.getUpperBoundIndex();
		lastLowerBoundIndex = Math.max(lastLowerBoundIndex, lowerBoundIndex);
		lastUpperBoundIndex = Math.min(lastUpperBoundIndex, upperBoundIndex);

		double x = 0;
		int previousYear = -1;
		int previousMonth = -1;
		double unitWidth = dateAxis.getUnitWidth();
		final double dateAxisWidth = dateAxis.getWidth();
		final boolean isPointAndFigure = BarType.POINT_AND_FIGURE.equals(getChart().getChartingView().getBarType());
		final boolean isEquiVolume = BarType.EQUIVOLUME.equals(getChart().getChartingView().getBarType());

		if (isPrimary()) {
			dateAxis.clear();
		}
		if (null != getPlotNode()) {
			getPlotNode().preLayout();
		}

		for (int index = lastLowerBoundIndex; index >= lastUpperBoundIndex; index--) {
			if ((0 <= index) && (dataProperty().getSize() > index)) {
				final Data<Scrip, Bar> data = dataProperty().get(index);
				if ((index <= lowerBoundIndex) && (index >= upperBoundIndex)) {
					if (isPrimary()) {

						if (null != data.getDataNode()) {
							data.getDataNode().getRectangle().setX(x);
							if (isEquiVolume) {
								unitWidth = (data.getExtraValue().getVolume() * dateAxisWidth) / volumeSum;
							}
							data.getDataNode().getRectangle().setWidth(unitWidth);
						}
						if (null != getPlotNode()) {
							getPlotNode().layout(data, x, unitWidth);
						}
						x += unitWidth;

						if(isPointAndFigure){
							primaryChart().getGrid().addVerticalLine(x);
						}
						
						calendar.setTimeInMillis(data.getDate());
						String text = null;
						final int currentYear = calendar.get(Calendar.YEAR);
						final int currentMonth = calendar.get(Calendar.MONTH);
						if (previousYear != currentYear) {
							text = yearDateFormat.format(calendar.getTime());
						} else if (previousMonth != currentMonth) {
							text = monthDateFormat.format(calendar.getTime());
						}
						if (StringUtil.hasText(text)) {
							dateAxis.addLabel(text, x - (unitWidth / 2), !isPointAndFigure);
						}
						previousYear = currentYear;
						previousMonth = currentMonth;
						
					} else {
						final int primaryIndex = dateAxis.getIndexForValue(data.getDate());
						if (primaryIndex >= 0) {
							final DataNode<Scrip, Bar> primaryDataNode = primaryPlot().getData().get(primaryIndex).getDataNode();
							if ((null != primaryDataNode) && primaryDataNode.isVisible()) {
								x = primaryDataNode.getRectangle().getX();
								if ((null != data.getDataNode()) && (null != primaryDataNode)) {
									data.getDataNode().getRectangle().setX(x);
									data.getDataNode().getRectangle().setWidth(primaryDataNode.getRectangle().getWidth());
								}
								if (null != getPlotNode()) {
									getPlotNode().layout(data, x, unitWidth);
								}
							}
						} else {
							if (null != data.getDataNode()) {
								data.getDataNode().setVisible(false);
							}
						}
					}
					if (null != data.getDataNode()) {
						data.getDataNode().setVisible(true);
						data.getDataNode().layoutChartChildren();
					}
				} else {
					if (null != data.getDataNode()) {
						data.getDataNode().setVisible(false);
					}
				}
			}
		}
		if (null != getPlotNode()) {
			getPlotNode().postLayout();
		}
		lastLowerBoundIndex = lowerBoundIndex;
		lastUpperBoundIndex = upperBoundIndex;
	}


	@Override
	public void updateDataNodes(final List<? extends Data<Scrip, Bar>> datas) {
		final BarType barType = isPrimary() ? getChart().getChartingView().getBarType() : BarType.LINE;
		switch (barType) {
			case HAIKIN_ASHI:
			case CANDLESTICK:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final Candle candle = new Candle(data);
					candle.updateColor();
					data.setDataNode(candle);
				}
				break;
			case EQUIVOLUME:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final PriceBox priceBox = new PriceBox(data);
					priceBox.updateColor();
					data.setDataNode(priceBox);
				}
				break;
			case HLC:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final HLC hlc = new HLC(data);
					hlc.updateColor();
					data.setDataNode(hlc);
				}
				break;
			case LINE:
				if (isPrimary()) {
					setPlotNode(null);
					for (int index = datas.size() - 1; index >= 0; index--) {
						final Data<Scrip, Bar> data = datas.get(index);
						final PriceLineDataNode line = new PriceLineDataNode(data);
						line.updateColor();
						data.setDataNode(line);
					}
				} else {
					for (final Data<Scrip, Bar> data : datas) {
						data.setDataNode(null);
					}
					setPlotNode(new LinePlotNode<>(this));
				}
				break;
			case AREA:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final AreaDataNode areaPlotNode = new AreaDataNode(data);
					areaPlotNode.updateColor();
					data.setDataNode(areaPlotNode);
				}
				break;
			case KAGI:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final KagiNode kagiNode = new KagiNode(data);
					kagiNode.updateColor();
					data.setDataNode(kagiNode);
				}
				break;
			case RENKO:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final RenkoNode renkoNode = new RenkoNode(data);
					renkoNode.updateColor();
					data.setDataNode(renkoNode);
				}
				break;
			case THREE_LINE_BREAK:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final LineBreakNode lineBreakNode = new LineBreakNode(data);
					lineBreakNode.updateColor();
					data.setDataNode(lineBreakNode);
				}
				break;
			case OHLC:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final OHLC ohlc = new OHLC(data);
					ohlc.updateColor();
					data.setDataNode(ohlc);
				}
				break;
			case POINT_AND_FIGURE:
				setPlotNode(null);
				for (int index = datas.size() - 1; index >= 0; index--) {
					final Data<Scrip, Bar> data = datas.get(index);
					final Pnf pnf = (Pnf) data.getExtraValue();
					data.setDataNode(pnf.isUp() ? new CrossPnfNode(data) : new CriclePnfNode(data));
				}
				break;
			default:
				throw new IllegalArgumentException("This should never happen");
		}
	}

	@Override
	public void load() {
		final Scrip scrip = getExtraValue();

		if (null != scrip) {
			new Thread() {
				@Override
				public void run() {
					setLoadedBars(Client.BAR.find(createBarCriteria()));
				};
			}.start();
		}
	}

	public void setLoadedBars(final List<Bar> loadedBars) {
		this.loadedBars = loadedBars;
		setBars(loadedBars);
	}

	public void setBars(final List<Bar> bars) {
		this.bars = translateBarType(bars);
		setData(createDatas(this.bars));
	}

	private BarCriteria createBarCriteria() {
		final BarCriteria barCriteria = chartingView().currentBarCriteria().clone();
		if (isPrimary()) {
			final Calendar calendar = Calendar.getInstance();
			barCriteria.setTo(calendar.getTime());
			chartingView().getSpan().add(calendar, -1);
			barCriteria.setFrom(calendar.getTime());
		} else {
			barCriteria.setScripId(getExtraValue().getId());
		}
		return barCriteria;
	}

	public List<Data<Scrip, Bar>> createDatas(final List<Bar> bars) {
		final List<Data<Scrip, Bar>> datas = new ArrayList<>();
		previousBackingDataMax = backingDataMax;
		previousBackingDataMin = backingDataMin;
		backingDataMax = Double.MIN_VALUE;
		backingDataMin = Double.MAX_VALUE;
		for (int index = 0; index < bars.size(); index++) {
			final Bar bar = bars.get(index);
			backingDataMax = Math.max(backingDataMax, bar.getHigh());
			backingDataMin = Math.min(backingDataMin, bar.getLow());
			datas.add(new Data<Scrip, Bar>(bar.getDate().getTime(), bar.getClose(), bar.getHigh(), bar.getLow(), bar, PricePlot.this, index));
		}
		return datas;
	}

	public List<Bar> translateBarType(final List<Bar> bars) {
		final ChartingView chartingView = getChart().getChartingView();
		switch (chartingView.getBarType()) {
			case CANDLESTICK:
				return bars;
			case EQUIVOLUME:
				return bars;
			case HLC:
				return bars;
			case LINE:
				return bars;
			case OHLC:
				return bars;
			case HAIKIN_ASHI:
				return BarTypeUtil.haikinAshi(bars);
			case THREE_LINE_BREAK:
				return BarTypeUtil.lineBreak(bars, chartingView.getLineBreakReversalAmount());
			case RENKO:
				if (!bars.isEmpty()) {
					return new ArrayList<>(BarTypeUtil.renko(bars, chartingView.getRenkoReversalSize(), chartingView.isRenkoReversalSizeInPercentage()));
				}
			case KAGI:
				if (!bars.isEmpty()) {
					return new ArrayList<>(BarTypeUtil.kagi(bars, chartingView.getKagiReversalSize(), chartingView.isKagiReversalSizeInPercentage()));
				}
			case POINT_AND_FIGURE:
				if (!bars.isEmpty()) {
					return BarTypeUtil.pnf(bars, chartingView.getPnfBoxSize(), chartingView.getPnfReversalAmount(), chartingView.isPnfBoxSizeInPercentage());
				}
			default:
				return bars;
		}
	}

	public void setData(final List<Data<Scrip, Bar>> datas) {
		StoxPlatform.runLater(new Runnable() {
			@Override
			public void run() {
				dataProperty().setAll(datas);
				if (isPrimary()) {
					updateCurrentBarCriteria();
					loadAll();
					chartingView().updateAxisBounds();
				}

				if ((0 == getChart().getValueAxis().getUpperBound()) && (0 == getChart().getValueAxis().getLowerBound())) {
					getChart().getChartingView().getZoom().reset();
				} else {
					double upperBound = getChart().getValueAxis().getUpperBound();
					upperBound = MathUtil.praportion(previousBackingDataMin, upperBound, previousBackingDataMax, backingDataMin, backingDataMax);
					double lowerBound = getChart().getValueAxis().getLowerBound();
					lowerBound = MathUtil.praportion(previousBackingDataMin, lowerBound, previousBackingDataMax, backingDataMin, backingDataMax);
					getChart().getValueAxis().updateAxisBounds(upperBound, lowerBound);
				}

				getChart().updateAxisBounds();
			}
		});
	}

	private void updateCurrentBarCriteria() {
		if (!getData().isEmpty()) {
			final Date from = getData().get(primaryPlot().getData().size() - 1).getExtraValue().getDate();
			final Date to = getData().get(0).getExtraValue().getDate();
			chartingView().currentBarCriteria().setFrom(from);
			chartingView().currentBarCriteria().setTo(to);
		}
	}

	private void loadAll() {
		for (final Plot<Scrip, Bar> plot : chartingView().getPrimaryChart().getPlots()) {
			plot.load();
		}
		for (final Chart<?, ?> chart : chartingView().getCharts()) {
			chart.load();
		}
	}


	public double getVolumeSum() {
		return volumeSum;
	}

	public Color getUpBarColor() {
		return upBarColor;
	}

	public void setUpBarColor(Color upBarColor) {
		this.upBarColor = upBarColor;
	}

	public Color getDownBarColor() {
		return downBarColor;
	}

	public void setDownBarColor(Color downBarColor) {
		this.downBarColor = downBarColor;
	}

}
